09. Coding: Game Class Functionality
## Adding Functionality to the Game Class
Your class only needs to define two additional methods before we can get started with the MiniMax algorithm: get_legal_moves()
and forecast_move()
. Finding legal moves uses the rules of the game to determine where each agent can go, then the forecast function can be used to expand the game tree along the branch corresponding to a legal move.
Finding Legal Moves
The first function, get_legal_moves()
takes no arguments and returns a list
(the tests will fail for any other type of collection) of moves available to the active player in the current state. The "active" player is the agent with initiative to move (e.g., on an empty board player 1 is the active player, and initiative alternates after each move played). According to the game rules, each player can move to any open square for their first move, and then to any open square along a row, column, or diagonal from their current position. (Note that players cannot jump or pass through blocked squares.)
Forecasting Moves
The second function, forecast_move()
takes a move (a pair of coordinates (x, y)
of the desired endpoint of the player) and return a new game state object (you should not mutate game state objects). Treating the game state as immutable makes it trivial to roll out and unwind each branch of the game tree (children nodes will simply be garbage collected when the caller returns). (Hint: check out the copy.deepcopy
module from the standard library to copy your board state.)
Note: Don't be afraid to add additional functions to the class or module to help complete the two required tasks.
If you get stuck, flip over to the solution to see one possible implementation.
Start Quiz:
class GameState:
def __init__(self):
# TODO: Copy your implementation from the previous quiz
pass
def get_legal_moves(self):
""" Return a list of all legal moves available to the
active player. Each player should get a list of all
empty spaces on the board on their first move, and
otherwise they should get a list of all open spaces
in a straight line along any row, column or diagonal
from their current position. (Players CANNOT move
through obstacles or blocked squares.) Moves should
be a pair of integers in (column, row) order specifying
the zero-indexed coordinates on the board.
"""
# TODO: implement this function!
pass
def forecast_move(self, move):
""" Return a new board object with the specified move
applied to the current game state.
Parameters
----------
move: tuple
The target position for the active player's next move
(e.g., (0, 0) if the active player will move to the
top-left corner of the board)
"""
# TODO: implement this function!
pass
from gamestate import *
print("Creating empty game board...")
g = GameState()
print("Getting legal moves for player 1...")
p1_empty_moves = g.get_legal_moves()
print("Found {} legal moves.".format(len(p1_empty_moves or [])))
print("Applying move (0, 0) for player 1...")
g1 = g.forecast_move((0, 0))
print("Getting legal moves for player 2...")
p2_empty_moves = g1.get_legal_moves()
if (0, 0) in set(p2_empty_moves):
print("Failed\n Uh oh! (0, 0) was not blocked properly when " +
"player 1 moved there.")
else:
print("Everything looks good!")
from copy import deepcopy
xlim, ylim = 3, 2 # board dimensions
class GameState:
"""
Attributes
----------
_board: list(list)
Represent the board with a 2d array _board[x][y]
where open spaces are 0 and closed spaces are 1
_parity: bool
Keep track of active player initiative (which
player has control to move) where 0 indicates that
player one has initiative and 1 indicates player 2
_player_locations: list(tuple)
Keep track of the current location of each player
on the board where position is encoded by the
board indices of their last move, e.g., [(0, 0), (1, 0)]
means player 1 is at (0, 0) and player 2 is at (1, 0)
"""
def __init__(self):
self._board = [[0] * ylim for _ in range(xlim)]
self._board[-1][-1] = 1 # block lower-right corner
self._parity = 0
self._player_locations = [None, None]
def forecast_move(self, move):
""" Return a new board object with the specified move
applied to the current game state.
Parameters
----------
move: tuple
The target position for the active player's next move
"""
if move not in self.get_legal_moves():
raise RuntimeError("Attempted forecast of illegal move")
newBoard = deepcopy(self)
newBoard._board[move[0]][move[1]] = 1
newBoard._player_locations[self._parity] = move
newBoard._parity ^= 1
return newBoard
def get_legal_moves(self):
""" Return a list of all legal moves available to the
active player. Each player should get a list of all
empty spaces on the board on their first move, and
otherwise they should get a list of all open spaces
in a straight line along any row, column or diagonal
from their current position. (Players CANNOT move
through obstacles or blocked squares.)
"""
loc = self._player_locations[self._parity]
if not loc:
return self._get_blank_spaces()
moves = []
rays = [(1, 0), (1, -1), (0, -1), (-1, -1),
(-1, 0), (-1, 1), (0, 1), (1, 1)]
for dx, dy in rays:
_x, _y = loc
while 0 <= _x + dx < xlim and 0 <= _y + dy < ylim:
_x, _y = _x + dx, _y + dy
if self._board[_x][_y]:
break
moves.append((_x, _y))
return moves
def _get_blank_spaces(self):
""" Return a list of blank spaces on the board."""
return [(x, y) for y in range(ylim) for x in range(xlim)
if self._board[x][y] == 0]
INSTRUCTOR NOTE:
The example solution is intended to be simple, rather than performant. Returning copies of the game state when forecasting moves has significant overhead (especially in Python), and returning a complete list of all legal moves is inefficient (a generator would be better), but both conventions simplify the underlying implementation & interface for the minimax algorithm. We will use a similar interface in the next project.
When performance matters, it is typical to use bitboards.